/*
 * #%L
 * java model
 * %%
 * Copyright (C) 2009 - 2011 prefabware.com Stefan Isele
 * %%
 * All Rights Reserved.
 * This program is dual-licensed under the GNU General Public license
 * or a commercial license.
 * You may select to use one or the other of these licenses.
 * To obtain a commercial license please contact Stefan Isele at www.stefan-isele.de
 * Any modifications to this file must keep this entire header intact.
 * Redistribution, except as permitted by the selected license, is prohibited.
 * #L%
 */
package com.prefabware.jmodel.annotation;

import java.util.HashMap;
import java.util.Map;

import com.prefabware.commons.Condition;
import com.prefabware.commons.Message;
import com.prefabware.commons.ObjectUtil;
import com.prefabware.commons.QualifiedName;
import com.prefabware.jmodel.JEnumeration;
import com.prefabware.jmodel.JEnumerationValue;
import com.prefabware.jmodel.JType;
import com.prefabware.jmodel.JTypeDeclaration;
import com.prefabware.jmodel.expression.literal.JArrayLiteral;
import com.prefabware.jmodel.expression.literal.JLiteral;
import com.prefabware.jmodel.expression.literal.JLiteralBase;

/**
 * an annotation class can be used to create the class of an annotation to
 * annotate a class, use JAnnotation
 * 
 * @author sisele_job
 * 
 */
public class JAnnotationClass extends JType {

	/**
	 * annotation values in can be one of primitive type, String, Class,
	 * annotation, enumeration or 1-dimensional arrays thereof in the jmodel a
	 * JAnnotationValue can be of a primitive type, String or of a JType based
	 * on a Class, annotation or enumeration to handle those two cases there are
	 * the JAnnotationValueTypeClass and JAnnotationValueTypeJType
	 * 
	 * @author sisele_job
	 * 
	 * @param <TYPE>
	 */
	public static interface JAnnotationValueType<TYPE> {
		/**
		 * @return the type of this value
		 */
		public TYPE getType();

		/**
		 * @return the java class of thes value of such an JAnnotation
		 */
		public Class<?> getValueClazz();
	}

	public static class JAnnotationValueTypeBase<TYPE> implements JAnnotationValueType<TYPE> {

		private TYPE type;
		private Class<?> clazz;

		public JAnnotationValueTypeBase(TYPE type, Class<?> clazz) {
			super();
			this.type = type;
			this.clazz = clazz;
		}

		@Override
		public TYPE getType() {
			return type;
		}

		@Override
		public Class<?> getValueClazz() {
			return clazz;
		}

	}

	public static class JAnnotationValueTypeClass extends JAnnotationValueTypeBase<Class<?>> {

		public JAnnotationValueTypeClass(Class<?> clazz) {
			super(clazz, clazz);
		}

	}

	public static class JAnnotationValueTypeJType extends JAnnotationValueTypeBase<JType> {

		public JAnnotationValueTypeJType(JType type) {
			super(type, JType.class);
		}

	}

	public static class JAnnotationValueTypeJAnnotation extends JAnnotationValueTypeBase<JAnnotationClass> {

		public JAnnotationValueTypeJAnnotation(JAnnotationClass type) {
			super(type, JAnnotation.class);
		}

	}

	public static class JAnnotationValueTypeJEnumeration extends JAnnotationValueTypeBase<JEnumeration> {

		public JAnnotationValueTypeJEnumeration(JEnumeration type) {
			super(type, JEnumerationValue.class);
		}

	}

	public static class JAnnotationMethod {
		public static JAnnotationValueType<?> create(Object type) {
			if (type instanceof Class<?>) {
				return new JAnnotationValueTypeClass((Class<?>) type);
			} else if (type instanceof JAnnotationClass) {
				return new JAnnotationValueTypeJAnnotation((JAnnotationClass) type);
			} else if (type instanceof JEnumeration) {
				// type=JEnumeration when reflecting enums because in java the type of
				// an enum field is the enum itself
				return new JAnnotationValueTypeJEnumeration((JEnumeration) type);
			} else if (type instanceof JEnumerationValue) {
				JEnumerationValue enumValueType = (JEnumerationValue) type;
				return new JAnnotationValueTypeJEnumeration((JEnumeration) enumValueType.getDeclaringType());
			} else if (type instanceof JType) {
				return new JAnnotationValueTypeJType((JType) type);
			} else
				throw new IllegalArgumentException(String.format("cannot create JAnnotationValueType for %s", type));

		}

		/**
		 * creates a JAnnotationValue for this method with the given name and
		 * value if the value is not of the type specified for this method, an
		 * exception is thrown
		 * 
		 * @param name
		 * @param value
		 * @return
		 */
		public JAnnotationValue<?> createValue(String name, Object value) {
			JLiteral literal2 = JLiteralBase.create(value);
			return createValue(name, literal2);
		}

		private JAnnotationValue<?> createValue(String name, JLiteral literal2) {
			if (literal2 instanceof JArrayLiteral) {
				return new JAnnotationValueArray(name, (JArrayLiteral) literal2);
			} else {
				return new JAnnotationValue<JLiteral>(name, literal2);
			}
		};

		private Object defaultValue;
		private String name;
		private JAnnotationValueType<?> type;

		public JAnnotationMethod(String name, Object defaultValue, JAnnotationValueType<?> type) {
			this.name = name;
			this.defaultValue = defaultValue;
			this.type = type;
		}

		public Object getDefaultValue() {
			return defaultValue;
		}

		/**
		 * can be a java primitive, String or a JType
		 * 
		 * @return
		 */
		public JAnnotationValueType<?> getType() {
			return type;
		}

		public String getName() {
			return name;
		}
	};

	private Map<String, JAnnotationMethod> annotationMethods;

	public JAnnotationClass(QualifiedName qn, JTypeDeclaration declaration) {
		super(qn, declaration);
		this.annotationMethods = new HashMap<String, JAnnotationClass.JAnnotationMethod>();
	}

	public JAnnotationMethod getAnnotationMethod(String name) {
		if (this.annotationMethods.containsKey(name)) {
			return annotationMethods.get(name);
		} else {
			return null;
		}
	}

	/**
	 * 
	 * @param value
	 * @return true, if the value was set and e.g should be included in asCode()
	 * to be set means, the value is not null and does not equal the default
	 * 
	 */
	public boolean isSet(JAnnotationValue<?> value) {
		Condition.notNull(value, ".. JAnnotationValue");
		JAnnotationMethod annotationMethod = this.getAnnotationMethod(value.getName());
		Condition.notNull(annotationMethod,
				Message.format("could not find JAnnotationMethod with name = %s in %s", value.getName(), this));
		if (value.getValue()==null) {
			return true;
		}
		Object defaultValue = annotationMethod.getDefaultValue();
		return ObjectUtil.equals(value.getValue(), defaultValue);
	}

	/**
	 * adds a new AnnotationMethod with the name and default
	 * 
	 * @param name
	 * @param defaultValue
	 * @param type
	 * @return
	 */
	public JAnnotationMethod addAnnotationMethod(String name, Object defaultValue, Object type) {
		JAnnotationValueType<?> valueType = JAnnotationMethod.create(type);
		JAnnotationMethod method = new JAnnotationMethod(name, defaultValue, valueType);
		this.annotationMethods.put(name, method);
		return method;
	}

	@Override
	public String toString() {
		return "JAnnotationClass [" + getQualifiedName() + "]";
	}

	@Override
	public String getJavaKeyword() {
		return "@interface";
	}

}
